通过 script 标签的 src 属性,在当前网页中全局引入 vue3 的脚本文件:
xxxxxxxxxx11<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>创建 vue3 的单页面应用程序实例:
xxxxxxxxxx91// 2.1 从 Vue 对象中解构出 createApp 函数2const { createApp } = Vue34// 2.2 调用 createApp 这个函数,就能够创建出一个单页面应用程序的实例5const app = createApp()67// 2.3 调用 app 实例对象上的 mount() 函数,8// 指定单页面应用程序 app,实际要控制页面上哪个区域的渲染9app.mount('#app')声明 vue3 的单页面应用程序实例,实际要控制的页面区域:
xxxxxxxxxx31<!-- 注意:如果内容为空,则 vue3 会在提示一个警告消息:2[Vue warn]: Component is missing template or render function. at <App> -->3<div id="app"></div>在调用 createApp() 函数时,可以提供一个对象作为配置参数,例如:
xxxxxxxxxx11const app = createApp({ /*配置对象*/ })如果想提供要渲染的数据,可以在步骤1的配置对象中,通过 data 节点提供渲染期间要使用的数据:
xxxxxxxxxx81const app = createApp({2 // 2.1 注意:data 节点是一个函数3 data() {4 // 2.2 在 data 函数内部,return 的这个对象,就是数据对象,5 // 要渲染的数据,可以直接写到这个对象中,例如 return { name: 'zs' }6 return {}7 }8})在步骤2的 data 节点中,定义一个名为 name 的数据,值是 liulongbin:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 name: 'liulongbin'5 }6 }7})在 vue3 控制的模板结构中,使用 {{ 数据名 }} 语法,把数据渲染出来:
xxxxxxxxxx31<div id="app">2 <h1>大家好,我是:{{ name }}</h1>3</div>拓展:当我们修改 data 节点下的数据后,即可看到页面上的 HTML 内容会自动被刷新。这就是 vue 的强大之处:数据驱动视图。修改 data 数据的示例代码如下:
xxxxxxxxxx11app._instance.proxy.name = 'escook'在 vue 中,指令是带有 v- 前缀的特殊 attribute,它是 vue 提供的特殊语法,大家有必要掌握 vue 中常用指令的使用。
指令能够辅助前端程序员高效地把数据渲染为 HTML 的结构,而程序员不需要调用任何操作 DOM 的 API。
插值表达式(又叫做:Mustache)的语法为 {{ }},vue 在解析模板期间,会把 {{ }} 所在的位置,替换为对应的数据值,例如:
xxxxxxxxxx11<h1>大家好,我是:{{ name }}</h1>vue 会把 name 的值,替换到 {{ name }} 所在的位置。
注意:插值表达式 {{ }} 是唯一一个不以 v- 前缀开头的指令。
v-text 指令用来填充 HTML 元素的内容,如果 HTML 元素内部有其它内容存在,则会被覆盖掉。语法格式如下:
xxxxxxxxxx11<h3 v-text="msg">是兄弟就来</h3>对应的数据为:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 msg: '砍我吧'5 }6 }7})注意:由于 v-text 指令存在覆盖已有内容的问题,所以在实际开发中它很少被用到。最常用的还是 {{ }} 插值表达式,因为它只是占位符,不会覆盖已有内容。
v-html 指令用来渲染带有 HTML 标记的文本内容,它可以把 HTML 标记解析为真正的 HTML 元素,并插入到模板中渲染。
而插值表达式和 v-text 指令只会把 HTML 标记渲染为纯文本,而不是 HTML。
v-html 的语法格式如下:
xxxxxxxxxx11<div v-html="rawHtml"></div>对应的数据为:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 rawHtml: '<span style="color: red;">少年强则国强</span>'5 }6 }7})v-bind 指令用来为元素的属性绑定动态的属性值。指令语法如下:
xxxxxxxxxx11<div v-bind:title="titleMsg">xxx</div>对应的数据为:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 titleMsg: '哇哈哈'5 }6 }7})又例如,为图片的 src 属性动态绑定属性的值:
xxxxxxxxxx11<img v-bind:src="url" />对应的数据为:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 url: 'https://img.yzcdn.cn/vant/cat.jpeg'5 }6 }7})在实际开发中,v-bind 指令的使用频率非常高,为了简化它的写法,vue 规定 v-bind 指令可以简写为英文的 : 且二者是完全等价的。如上面的例子可以使用 : 简写为:
xxxxxxxxxx21<div :title="titleMsg">xxx</div>2<img :src="url" />对应的数据为:
xxxxxxxxxx81const app = createApp({2 data() {3 return {4 titleMsg: '哇哈哈',5 url: 'https://img.yzcdn.cn/vant/cat.jpeg'6 }7 }8})注意:今后在 vue 项目开发中,只要看到某个属性前面出现了英文的 : 那么,一定是为这个属性绑定了动态的值。
在 vue 中,某些属性的取值可以是布尔值 true 或 false,表示当前的属性是否应该应用于当前的元素。例如 disabled 属性:
xxxxxxxxxx51<!-- 禁用按钮A -->2<button :disabled="true">按钮A</button>34<!-- 不禁用按钮B -->5<button :disabled="false">按钮B</button>与之类似的,还有 radio 和 checkbox 的 checked 属性:
xxxxxxxxxx81<!-- 默认选中“男” -->2<input type="radio" name="gender" :checked="true">男3<input type="radio" name="gender">女45<!-- 默认选中“足球”和“乒乓球” -->6<input type="checkbox" name="hobby">篮球7<input type="checkbox" name="hobby" :checked="true">足球8<input type="checkbox" name="hobby" :checked="true">乒乓球另外,表单元素 select 下的 option 选项的 selected 属性,也可以绑定布尔值:
xxxxxxxxxx51<select>2 <option value="北京">北京</option>3 <option value="上海" :selected="true">上海</option>4 <option value="广州">广州</option>5</select>如果要为某个元素同时绑定多个动态的属性值,可以把多个动态属性封装为一个 JavaScript 对象:
xxxxxxxxxx121const app = createApp({2 data() {3 return {4 // propObj 对象中封装了一系列属性的键值对5 attrsObj: {6 id: 'box',7 class: 'container',8 title: '布局容器'9 }10 }11 }12})通过不带参数的 v-bind 指令,即可方便的把 attrsObj 对象中封装的属性,一次性绑定到对应的元素上:
xxxxxxxxxx11<div v-bind="attrsObj">顶部 header 区域</div>注意:不带参数的 v-bind 指令,指的是省略了
:属性名的用法。
在 vue 的数据绑定中,除了支持简单的属性名绑定之外,还支持完整的 JavaScript 表达式绑定。
例如,以下这些都属于简单的属性名绑定,它们是直接把 data 中数据项的名字,绑定到了模板中:
xxxxxxxxxx51<div>我是:{{ name }}</div>23<div v-text="msg"></div>45<img :src="url" />除此之外,还支持表达式的绑定,例如:
xxxxxxxxxx111<!-- 函数的调用 & 数学运算 -->2<div>我是:{{ name.toUpperCase() }},我今年{{ age + 1 }}岁了。</div>34<!-- 函数的调用 -->5<div v-text="msg.split('').reverse().join('')"></div>67<!-- 字符串的拼接 -->8<img :src="'https://img.yzcdn.cn/vant/' + url" />910<!-- 三元表达式 -->11<div>{{ age >= 18 ? '抽烟喝酒烫头' : '可乐牛奶娃哈哈' }}</div>对应的数据如下:
xxxxxxxxxx101const app = createApp({2 data() {3 return {4 name: 'liulongbin',5 age: 17,6 msg: '冯绍峰',7 url: 'cat.jpeg'8 }9 }10})v-model 双向绑定指令,简化了表单元素的赋值和取值操作。
v-model 的作用:
input 元素通过 v-model 指令,可以方便地进行赋值和取值,示例代码如下:
xxxxxxxxxx21<p>Message 的值是:{{ message }}</p>2<input type="text" v-model="message">对应的数据如下:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 message: 'hello'5 }6 }7})textarea 元素通过 v-model 指令,可以方便地进行赋值和取值,示例代码如下:
xxxxxxxxxx31<p>Message 的值是:</p>2<pre>{{ message }}</pre>3<textarea v-model="message"></textarea>对应的数据如下:
xxxxxxxxxx81const app = createApp({2 data() {3 return {4 // 注意:这里的 \n 是换行符5 message: 'hello \nworld.'6 }7 }8})单一复选框的双向绑定,绑定的是布尔类型的值:
xxxxxxxxxx21<p>复选框选中的flag值为:{{flag}}</p>2<input type="checkbox" v-model="flag">对应的数据如下:
xxxxxxxxxx81const app = createApp({2 data() {3 return {4 // 是否被选中5 flag: false6 }7 }8})多个复选框的双向绑定,绑定的是数组类型的值,而且每个 checkbox 必须通过 value 属性提供选中项的值:
xxxxxxxxxx41<p>多个复选框选中的 hobbies 值为:{{ hobbies }}</p>2<label><input type="checkbox" v-model="hobbies" value="篮球">篮球</label>3<label><input type="checkbox" v-model="hobbies" value="足球">足球</label>4<label><input type="checkbox" v-model="hobbies" value="冰球">冰球</label>对应的数据如下:
xxxxxxxxxx81const app = createApp({2 data() {3 return {4 // 选中的值5 hobbies: []6 }7 }8})单选按钮的特点是多选一,所以对单选按钮进行双向绑定时,需要把多个单选按钮通过 v-model 指令绑定到同一个数据源,并通过 value 属性指定选中后的值:
xxxxxxxxxx31<p>单选按钮选中的 gender 值为:{{ gender }}</p>2<label><input type="radio" v-model="gender" value="男">男</label>3<label><input type="radio" v-model="gender" value="女">女</label>对应的数据如下:
xxxxxxxxxx81const app = createApp({2 data() {3 return {4 // 选中的值5 gender: '男'6 }7 }8})单选选择器的双向绑定,只允许选中一个值:
xxxxxxxxxx71<p>选中的城市为:{{ city }}</p>2<select v-model="city">3 <option value="">请选择</option>4 <option value="beijing">北京</option>5 <option value="shanghai">上海</option>6 <option value="nanjing">南京</option>7</select>对应的数据如下:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 city: ''5 }6 }7})多选选择器的双向绑定,允许选中多个值,所以需要绑定数组格式的数据源:
xxxxxxxxxx61<p>选中的城市为:{{ areas }}</p>2<select v-model="areas" multiple>3 <option value="shunyi">顺义区</option>4 <option value="haidian">海淀区</option>5 <option value="daxing">大兴区</option>6</select>对应的数据如下:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 areas: []5 }6 }7})默认情况下,v-model 会在每次 input 事件后更新数据。可以添加 .lazy 修饰符来改为在每次 change 事件后更新数据:
xxxxxxxxxx11<input v-model.lazy="msg" />如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:
xxxxxxxxxx11<input v-model.number="age" />注意:
parseFloat() 处理,那么将返回原始值。number 修饰符会在输入框有 type="number" 时自动启用。如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:
xxxxxxxxxx11<input v-model.trim="msg" />条件渲染指令用来条件性地渲染页面上的某一部分内容。只有表达式的条件成立,才会真正渲染这一部分的内容。
常用的条件渲染指令是 v-if、v-else 和 v-else-if。其中,v-if 指令可以单独使用,也可以结合 v-else 和 v-else-if 指令实现两个或多个条件的按需渲染。
v-if 的语法格式如下:
xxxxxxxxxx11<div v-if="表达式"></div>其中,只有表达式的返回值为 true 时,才会真正渲染被 v-if 指令控制的 div 元素。
如果 v-if 的表达式返回值为 false,则被 v-if 指令控制的 div 不会被渲染到浏览器中。
例如:
xxxxxxxxxx11<div v-if="flag">无敌是多么的寂寞</div>对应的数据为:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 flag: true5 }6 }7})v-if 指令可以结合 v-else 指令一起使用。
当条件为真时渲染被 v-if 指令控制的元素,当条件为假时渲染被 v-else 指令控制的元素。例如:
xxxxxxxxxx21<div v-if="age >= 18">抽烟喝酒烫头</div>2<div v-else>牛奶可乐娃哈哈</div>注意:v-else 指令不需要通过 = 指定相应的表达式,因为 v-else 是兜底的条件,只要前面的所有条件都不满足,那么必然会触发 v-else 的执行。
v-if 指令可以结合 v-else-if 和 v-else 指令一起使用,从而组成复杂的条件渲染逻辑。
当 v-if 或某个 v-else-if 相应的条件为真时,被控制的元素才会被渲染。
最后的 v-else 依然是兜底的条件,当所有的 v-if 和 v-else-if 条件都不成立时,才会触发 v-else 的执行。例如:
xxxxxxxxxx41<div v-if="score === 'A'">优秀</div>2<div v-else-if="score === 'B'">良好</div>3<div v-else-if="score === 'C'">一般</div>4<div v-else>差</div>对应的数据为:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 score: 'A'5 }6 }7})正常情况下 v-if 指令只能控制单个元素的显示和隐藏。如果需要使用 v-if 控制一组元素的显示和隐藏,就需要在这一组元素之外包裹一个 div 作为容器,并将 v-if 指令应用于 div 容器之上,例如:
xxxxxxxxxx51<div v-if="true">2 <h1>咏鹅</h1>3 <p>鹅鹅鹅,曲项向天歌。</p>4 <p>白毛浮绿水,红掌拨清波。</p>5</div>这么做虽然能实现需求,但会在页面上渲染出一个多余的 div 容器。
更好的方案是使用 vue 内置的 <template> 元素作为外层包裹性质的容器,因为它不会被渲染为实际的元素,只起到包裹性质的作用。例如:
xxxxxxxxxx51<template v-if="true">2 <h1>咏鹅</h1>3 <p>鹅鹅鹅,曲项向天歌。</p>4 <p>白毛浮绿水,红掌拨清波。</p>5</template>另一个可以用来实现条件渲染的指令是 v-show。它的语法格式如下:
xxxxxxxxxx11<h1 v-show="flag">Hello!</h1>如果表达式的值为 true,则被控制的元素会被显示;
如果表达式的值为 false,则被控制的元素会被隐藏。
注意:v-show 指令不支持在 元素上使用,也不能和 v-else 搭配使用。
相同点:
v-if 和 v-show 指令都能控制元素的条件渲染。
不同点:
控制元素显示和隐藏的
手段不同
:
v-if 指令会动态创建和删除被控制的元素,从而达到切换元素显示和隐藏的目的;v-show 指令仅切换了被控制元素上名为 display 的 CSS 属性,从而达到切换元素显示和隐藏的目的;初始渲染
的
性能
不同:
频繁切换
时的
性能
不同:
总结:
v-if 有更高的切换开销v-show 有更高的初始渲染开销为了响应用户对 DOM 元素的操作,vue 提供了事件绑定指令 v-on(简写为 @)。
当监听到 DOM 事件的触发时,会执行对应的 JavaScript 逻辑。它的语法格式为 v-on:事件名="handler" 或 @事件名="handler"。例如:
xxxxxxxxxx51<!-- v-on 是事件绑定指令 -->2<button v-on:click="show">按钮</button>34<!-- @ 是 v-on 指令的简写形式 -->5<button @click="show">按钮</button>上述代码演示了如何为 button 按钮绑定 click 点击事件。
除此之外,vue 还支持绑定其它类型的事件,这里就不再一一例举了。因为把 DOM 原生事件前面的 on 替换成 v-on: 或 @ 就变成了 vue 的事件绑定形式,例如:
方法事件处理器指的是:指定一个方法作为事件的处理器。例如下面的代码所示,指定了一个 show 方法作为 click 事件的处理器:
xxxxxxxxxx11<button @click="show">按钮</button>show 方法作为事件处理器,需要定义在 methods 节点下,例如:
xxxxxxxxxx111const app = createApp({2 data() {3 return {}4 },5 methods: {6 show(event) { 7 console.log('ok')8 console.log(event.target.tagName)9 }10 }11})在方法事件处理器的参数列表中,第一个形参 event 是事件对象。
声明模板结构如下:
xxxxxxxxxx21<p>count的值为:{{ count }}</p>2<button @click="add">+1</button>在 data 中声明数据源 count,在 methods 中声明事件处理器 add,代码如下:
xxxxxxxxxx121const app = createApp({2 data() {3 return {4 count: 05 }6 },7 methods: {8 add() {9 app._instance.proxy.count++10 }11 }12})注意:methods 节点下的方法中,this 指向的就是 app._instance.proxy。所以上述代码完全可以替换为 this.count++
内联事件处理器相当于原生 DOM 中的内联 JavaScript,例如数值自增的操作,可以简写成内联事件处理器的形式:
xxxxxxxxxx21<p>count的值为:{{ count }}</p>2<button @click="count++">+1</button>对应的数据为:
xxxxxxxxxx71const app = createApp({2 data() {3 return {4 count: 05 }6 }7})注意:内联事件处理器通常用于简单的业务场景,如果涉及到复杂的业务逻辑,请使用方法事件处理器或在内联处理器中调用方法。
首先,我们要能够明确的区分开方法事件处理器和内联事件处理器。
如果事件绑定的处理器是个纯粹的方法名,则是方法事件处理器,例如:
xxxxxxxxxx11<button @click="show">按钮A</button>除此之外,其它绑定事件处理器的形式,都是内联事件处理器,例如:
xxxxxxxxxx81<!-- 绑定内联的 JavaScript -->2<button @click="count++">按钮C</button>34<!-- 绑定了一个方法的调用 -->5<button @click="show()">按钮B</button>67<!-- 绑定了方法的调用的同时,传递参数 -->8<button @click="show('Hello world.')">按钮B</button>内联事件处理器的优点:解锁了模板向处理器方法传递参数的能力。
内联事件处理器的缺点:事件对象丢失了,无法在处理器方法中访问到事件对象 event。
上述问题的解决方案有两个,分别是:
解决方案1:使用特殊的 $event 变量
xxxxxxxxxx11<button @click="showMsg('hello world.', $event)">按钮</button>对应的 showMsg 处理器为:
xxxxxxxxxx101const app = createApp({2 methods: {3 showMsg(msg, event) {4 // 改变按钮显示的文本5 event.target.innerHTML = msg6 // 改变按钮的背景颜色7 event.target.style.backgroundColor = 'cyan'8 }9 }10})解决方案2:使用内联箭头函数接收并传递 event 对象
xxxxxxxxxx11<button @click="(event) => showMsg('你好,世界。', event)">按钮</button>对应的 showMsg 处理器为:
xxxxxxxxxx101const app = createApp({2 methods: {3 showMsg(msg, event) {4 // 改变按钮显示的文本5 event.target.innerHTML = msg6 // 改变按钮的背景颜色7 event.target.style.backgroundColor = 'cyan'8 }9 }10})在原生 DOM 的事件处理函数中,如果想要阻止冒泡行为,则需要调用 event.stopPropagation();如果想要阻止默认行为,则需要调用 event.preventDefault()。为了提高用户的开发体验,vue 提供了更优雅的方式来阻止事件冒泡或默认行为,即:事件修饰符。
在 vue 中最常用的两个事件修饰符分别是:
其中 .prevnet 用来阻止默认行为,例如:
xxxxxxxxxx31<!-- 使用 .prevent 修饰了 a 链接的 click 事件 -->2<!-- 点击超链接后,会阻止超链接的默认跳转行为 -->3<a href="https://www.escook.cn/" @click.prevent="showMsg">超链接</a>另外 .stop 用来阻止事件冒泡,例如:
xxxxxxxxxx51<div @click="outerHandler">2 <!-- 点击内部的 button 按钮,click 事件不会向外冒泡 -->3 <!-- 所以外层的 outerHandler 处理器不会执行 -->4 <button @click.stop="innerHandler">按钮</button>5</div>拓展:其它事件修饰符还有 .self、.capture、.once、.passive。具体用法请参考 vue3 官方文档 – 事件修饰符。
在监听键盘事件时,我们经常需要检查特定的按键,从而执行特定的操作。例如:
示例代码如下:
xxxxxxxxxx11<input type="text" v-model="msg" @keyup.enter="submit" @keyup.esc="clear">对应的 JS 处理逻辑为:
xxxxxxxxxx171const app = createApp({2 data() {3 return {4 msg: '' // 文本框的数据5 }6 },7 methods: {8 // 该处理函数仅在用户按下 enter 键时触发9 submit() {10 console.log('提交的数据为:' + this.msg)11 },12 // 该处理函数仅在用户按下 esc 键时触发13 clear() {14 this.msg = ''15 }16 }17})vue 为常用的按键提供了官方内置的按键别名,列表如下:
如果上述列表中没有你想监听的按键,则可以使用 $event.key 先获取按键的名称,再把获取到的按键名称转为 kebab-case 形式,最后利用转换得到的按键名进行监听即可,例如下面的代码监听了 CapsLock 按键:
xxxxxxxxxx21<p>输入状态:{{ isUpperCase ? '大写' : '小写' }}</p>2<input type="text" v-model="msg" @keyup.caps-lock="changeMode">对应的 JS 逻辑为:
xxxxxxxxxx141const app = createApp({2 data() {3 return {4 msg: '', // 文本框的数据5 isUpperCase: false // 是否为大写输入模式6 }7 },8 methods: {9 // 仅当用户按下的是 CapsLock 键,才触发此函数的执行10 changeMode() {11 this.isUpperCase = !this.isUpperCase12 }13 }14})如果在触发事件的时候,想要判断用户是否同时按下了 Ctrl、Alt 等系统按键。此时可以使用 vue 内置的系统按键修饰符,主要有以下4个:
例如,下面的代码监听了触发 div 的 click 事件时,是否同时按下了特定的系统按键,从而改变 div 的形状和外观:
xxxxxxxxxx91<!-- 点击 div 的时候, -->2<!-- 1. 如果同时按下了 Ctrl 键,则添加 square 类样式 -->3<!-- 2. 如果同时按下了 Alt 键,则添加 round 类样式 -->4<!-- 3. 如果同时按下了 Shift 键,则还原为默认的 box 类样式 -->5<div class="box" :class="shape"6 @click.ctrl="changeShape('square')"7 @click.alt="changeShape('round')"8 @click.shift="changeShape('')">9</div>对应的 JS 逻辑为:
xxxxxxxxxx141const app = createApp({2 data() {3 return {4 // 类样式的名称5 shape: ''6 }7 },8 methods: {9 // 事件的处理函数10 changeShape(shape) {11 this.shape = shape12 }13 }14})配套的 CSS 样式为:
xxxxxxxxxx201<style>2 .box {3 width: 300px;4 height: 300px;5 background-color: #efefef;6 transition: all 1s ease;7 }89 .square {10 border-radius: 20px;11 background-color: cyan;12 transition: all 1s ease;13 }1415 .round {16 border-radius: 50%;17 background-color: lightgreen;18 transition: all 1s ease;19 }20</style>.exact 修饰符上述的例子中,存在一个很明显的 Bug:
Ctrl 或 Alt 或 Shift 按键时,才触发 changeShape 函数Ctrl + Alt 的组合按键,也会触发 changeShape 函数而 .exact 修饰符可以完美的解决这个问题。.exact 修饰符表示精确匹配系统按键。
因此,我们可以针对上述的例子进行修改,在特定的系统按键修饰符的后面应用 .exact 修饰符,表示精确匹配系统按键:
xxxxxxxxxx51<div class="box" :class="shape" 2 @click.ctrl.exact="changeShape('square')" 3 @click.alt.exact="changeShape('round')"4 @click.shift.exact="changeShape('')">5</div>注意:所谓的精确匹配系统按键,仅对系统按键修饰符生效,如果用户按下了 Ctrl + A 的组合键,也会触发
@click.ctrl.exact所绑定的事件处理器。
vue 还提供了鼠标按键修饰符,用来监听事件是否由特定的鼠标按键触发:
例如,下面的代码演示了如何阻止在 h1 元素上显示鼠标的右键菜单:
xxxxxxxxxx11<h1 @click.right.prevent>这是一个标题</h1>注意:绑定事件时,不一定非要提供事件的处理器,我们也可只提供事件修饰符,从而达到特定的目的。
v-for 指令是 vue 提供的列表渲染指令。
如果您有一个数组,想把数组中的每一项渲染为格式相似的 HTML 结构,那么 v-for 指令可以帮助您实现列表数据的渲染。
使用场景:商品列表、用户列表等。
v-for 的基本语法格式为:
xxxxxxxxxx11v-for="当前循环项 in 数组"其中关键字 in 前面的是当前循环项,关键字 in 后面的要循环的数组。例如:
xxxxxxxxxx31<ul>2 <li v-for="item in list">姓名:{{ item.name }},年龄:{{ item.age }}</li>3</ul>对应的数据为:
xxxxxxxxxx121const app = createApp({2 data() {3 return {4 // 数组5 list: [6 { name: 'zs', age: 20 },7 { name: 'liulongbin', age: 21 },8 { name: 'escook', age: 22 }9 ]10 }11 }12})v-for 的完整语法格式为:
xxxxxxxxxx11v-for="(循环项, 循环项的索引) in 数组"其中 in 关键字左侧的 ( ) 里面,分别是当前循环项和当前循环项的索引。例如:
xxxxxxxxxx31<ul>2 <li v-for="(item, index) in goods">{{ index + 1 }}. {{ item }}</li>3</ul>对应的数据为:
xxxxxxxxxx81const app = createApp({2 data() {3 return {4 // 数组5 goods: ['手表', '手机', '手串']6 }7 }8})注意:v-for 中的索从 0 开始递增。
如果 v-for 指令中的循环项 item 是一个对象,则可以在 v-for 指令中进行解构操作,语法格式为:
xxxxxxxxxx31v-for="{数据A, 数据B} in 数组"2或3v-for="({数据A, 数据B}, 索引) in 数组"其中 in 关键字左侧的 { } 表示解构操作。例如:
xxxxxxxxxx31<ul>2 <li v-for="{name, age} in list">姓名:{{ name }},年龄:{{ age }}</li>3</ul>对应的数据为:
xxxxxxxxxx121const app = createApp({2 data() {3 return {4 // 数组5 list: [6 { name: 'zs', age: 20 },7 { name: 'liulongbin', age: 21 },8 { name: 'escook', age: 22 }9 ]10 }11 }12})v-for 指令每次只能循环生成一个元素,如果想在每次循环期间生成一组元素,则必须在这一组元素之外包裹一层 div 标签作为容器,并把 v-for 指令作用于外层的 div 容器之上。例如:
xxxxxxxxxx61<ul>2 <div v-for="(item, index) in list">3 <li class="divider" v-if="index!== 0"></li>4 <li>姓名:{{ item.name }},年龄:{{ item.age }}</li>5 </div>6</ul>对应的 css 样式为:
xxxxxxxxxx71<style>2 .divider {3 border-top: 1px solid #888;4 list-style: none;5 margin: 10px 0;6 }7</style>如上,虽然可以实现列表数据的渲染,但却不尽完美。因为 div 标签在循环中只起到容器的作用,在整个列表结构中没有任何意义。
所以推荐的做法是利用 vue 内置的 <template> 标签替代上述的 div 标签,因为 <template> 是一个虚拟的容器,不会被渲染为实际的元素。
最终,优化过后的代码如下:
xxxxxxxxxx61<ul>2 <template v-for="(item, index) in list">3 <li class="divider" v-if="index!== 0"></li>4 <li>姓名:{{ item.name }},年龄:{{ item.age }}</li>5 </template>6</ul>思考:分割线的
能否放在数据的 之后?如果能,则 v-if 的条件是什么?如果不能,请说出为什么。
注意:vue 官方不推荐在一个元素上,同时使用 v-if 和 v-for 指令。因为这样使用无法明确体现出二者的优先级。降低代码的阅读性和维护性。
当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 比 v-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:
xxxxxxxxxx61<ul>2 <!-- 注意:这里的 v-if 指令中,无法访问到 item.done 对应的数据, -->3 <!-- 因为 v-if 比 v-for 的优先级高, -->4 <!-- 当 v-if 执行的时候访问不到 item 对象,因为 v-for 此时还未执行! -->5 <li v-for="item in todos" v-if="!item.done">{{item.task}}</li>6</ul>对应的数据为:
xxxxxxxxxx131const app = createApp({2 data() {3 return {4 // 任务列表,done 为 true 表示完成;done 为 false 表示未完成5 todos: [6 { task: '晨练', done: true },7 { task: '吃早餐', done: true },8 { task: '吃午饭', done: false },9 { task: '午休', done: false }10 ]11 }12 }13})解决的方案很简单,先循环再判断即可。在 li 元素的外层包裹一个 template 组件,并把 v-for 指令从 li 上挪到 template 组件上,示例代码如下:
xxxxxxxxxx51<ul>2 <template v-for="item in todos">3 <li v-if="!item.done">{{item.task}}</li>4 </template>5</ul>改造后的代码除了解决了 v-if 优先级高导致的报错问题之外,还有这3个明显的特征:
<template> 是一个虚拟容器,不会被渲染为任何实际元素,因此不会导致 DOM 结构的冗余item.done 值为 false,则不会渲染对应的 DOM 结构,因此初始的渲染性能较好key 值是 v-for 指令中很重要的一个内容,在接下来的课程中,我们先来学习 key 值的基本使用,然后再深入探讨一下 key 值的内部原理。
在 data 节点下,定义任务列表的 todos 数据:
xxxxxxxxxx101data() {2 return {3 // 任务列表4 todos: [5 { task: '晨练' },6 { task: '吃早餐' },7 { task: '吃午饭' }8 ]9 }10}并在模板结构中,使用 v-for 指令循环渲染出列表的结构:
xxxxxxxxxx61<!-- 列表区域 -->2<ul>3 <li v-for="item in todos">4 <span>{{ item.task }}</span>5 </li>6</ul>为了能够实现在任务列表的头部添加新任务的功能,还需要在 data 节点下定义名为 taskName 的数据项,表示即将添加到头部的新任务的名称,同时,还要在 methods 节点下定义名为 add 的方法,用来当做添加按钮的 click 事件处理器:
xxxxxxxxxx251const app = createApp({2 data() {3 return {4 // 要添加的任务名称5 taskName: '',6 // 任务列表7 todos: [8 { task: '晨练' },9 { task: '吃早餐' },10 { task: '吃午饭' }11 ]12 }13 },14 methods: {15 // 添加操作16 add() {17 // 1. 如果要添加的任务名称为空,则提示警告信息18 if (this.taskName.length === 0) return console.warn('任务名称不能为空!')19 // 2. 向列表头部添加一个新任务20 this.todos.unshift({ task: this.taskName })21 // 3. 清空文本框的内容22 this.taskName = ''23 }24 }25})最后,在模板结构中,新增添加区域的 UI 结构如下:
xxxxxxxxxx161<div id="app">23 <!-- 添加区域 -->4 <div>5 <input type="text" v-model.trim="taskName">6 <button @click="add">添加</button>7 </div>89 <hr>10 <!-- 列表区域 -->11 <ul>12 <li v-for="item in todos">13 <span>{{ item.task }}</span>14 </li>15 </ul>16</div>至此,在头部追加新任务的功能就实现了,经过测试,代码没有任何问题,功能也可以正常使用。
我们在上一个案例的基础之上,继续添加一个勾选任务完成状态的新功能。
修改模板结构中的列表区域,在每个 li 元素中,都添加一个 checkbox 复选框,作为当前任务的完成状态,代码如下:
xxxxxxxxxx91<!-- 列表区域 -->2<ul>3 <li v-for="item in todos">4 <!-- 任务的完成状态 -->5 <input type="checkbox">6 <!-- 任务的名称 -->7 <span>{{ item.task }}</span>8 </li>9</ul>改造完成后立即测试当前的功能,我们会发现复选框的勾选状态发生了紊乱。
不用担心,这个问题是由 vue 默认的更新策略所导致的。具体原因我们需要在原理层面进行深入讲解。
那么接下来,我们首先来看一下如何解决这个勾选状态紊乱的问题。
解决方案,就是把每个复选框的勾选状态和数据源进行绑定,这样 vue 就能够正确更新每个复选框的勾选状态了。
首先,修改 data 下的数据节点,为 todos 下的每个任务,都添加一个 done 属性,用来表示当前任务是否完成:
xxxxxxxxxx121data() {2 return {3 // 要添加的任务名称4 taskName: '',5 // 任务列表6 todos: [7 { task: '晨练', done: true },8 { task: '吃早餐', done: false },9 { task: '吃午饭', done: false }10 ]11 }12}其次,修改 methods 下的 add 方法,在每次添加新任务时,为其附加一个 done: false 的初始完成状态:
xxxxxxxxxx111methods: {2 // 添加操作3 add() {4 // 1. 如果要添加的任务名称为空,则提示警告信息5 if (this.taskName.length === 0) return console.warn('任务名称不能为空!')6 // 2. 向列表头部添加一个新任务7 this.todos.unshift({ task: this.taskName })8 // 3. 清空文本框的内容9 this.taskName = ''10 }11}最后,修改模板结构,为每个任务中的 checkbox 复选框进行 v-model 的双向数据绑定:
xxxxxxxxxx91<!-- 列表区域 -->2<ul>3 <li v-for="item in todos">4 <!-- 任务的完成状态 -->5 <input type="checkbox" v-model="item.done">6 <!-- 任务的名称 -->7 <span>{{ item.task }}</span>8 </li>9</ul>至此,复选框勾选状态紊乱的问题得到了解决。
但是…如果再为每个任务添加一个文本框的备注呢?文本框的状态会紊乱吗?又该如何解决呢?
我们继续为案例添加新功能,即为每个任务添加一个文本框的作为备注信息。
修改模板结构中的列表区域,在每个 li 元素中,都添加一个文本框,作为当前任务的备注描述,代码如下:
xxxxxxxxxx111<!-- 列表区域 -->2<ul>3 <li v-for="item in todos">4 <!-- 任务的完成状态 -->5 <input type="checkbox" v-model="item.done">6 <!-- 任务的名称 -->7 <span>{{ item.task }}</span>8 <!-- 文本框备注 -->9 <input type="text">10 </li>11</ul>改造完成后立即测试当前的功能,我们会发现在文本框中填写的备注也发生了紊乱。
Don’t worry,这个问题依然是由 vue 默认的更新策略所导致的。具体原因稍后详细介绍。
我们还是先来看一下如何解决这个文本框内容紊乱的问题吧。
本次的解决方案和解决复选框勾选状态紊乱的方案如出一辙,核心思路就是把文本框的内容和数据源进行双向的绑定即可。
首先,修改 data 下的数据节点,为 todos 下的每个任务,都添加一个 remark 属性,用来表示当前任务的备注描述:
xxxxxxxxxx121data() {2 return {3 // 要添加的任务名称4 taskName: '',5 // 任务列表6 todos: [7 { task: '晨练', done: true, remark: '早晨6点' },8 { task: '吃早餐', done: false, remark: '' },9 { task: '吃午饭', done: false, remark: '' }10 ]11 }12}其次,修改 methods 下的 add 方法,在每次添加新任务时,为其附加一个 remark: '' 的初始备注描述:
xxxxxxxxxx111methods: {2 // 添加操作3 add() {4 // 1. 如果要添加的任务名称为空,则提示警告信息5 if (this.taskName.length === 0) return console.warn('任务名称不能为空!')6 // 2. 向列表头部添加一个新任务7 this.todos.unshift({ task: this.taskName, done: false, remark: '' })8 // 3. 清空文本框的内容9 this.taskName = ''10 }11}最后,修改模板结构,为每个任务中的文本框进行 v-model 的双向数据绑定:
xxxxxxxxxx111<!-- 列表区域 -->2<ul>3 <li v-for="item in todos">4 <!-- 任务的完成状态 -->5 <input type="checkbox" v-model="item.done">6 <!-- 任务的名称 -->7 <span>{{ item.task }}</span>8 <!-- 文本框备注 -->9 <input type="text" v-model="item.remark">10 </li>11</ul>至此,文本框内容紊乱的问题也得到了解决。到此为止,我们已经连续两次解决了状态紊乱的问题,是时候做一个小小的总结了:
如果列表每一项中的表单元素,没有绑定任何数据源,则必然发生数据紊乱的问题
如果列表每一项中的表单元素,和数据源进行了绑定,则数据紊乱的问题必然得到解决
总之,临时状态有可能会发生状态紊乱的问题,而响应式状态不会发生状态紊乱的问题。大家先记住这种表象式的总结,后面会有助于大家在原理层面深入理解这个问题。
我们在之前解决临时状态紊乱时,每次都需要为表单元素进行数据的绑定,从而把临时状态变更为响应式状态。
有没有其它方案,能让我们在不改变临时状态的前提下,也能解决状态紊乱的问题呢?这就需要用到 v-for 中的 key 值了。
首先,让我们把页面结构重新梳理一遍,把临时状态紊乱的问题完整的还原出来,data 中的数据如下:
xxxxxxxxxx121data() {2 return {3 // 要添加的任务名称4 taskName: '',5 // 任务列表6 todos: [7 { task: '晨练' },8 { task: '吃早餐' },9 { task: '吃午饭' }10 ]11 }12}methods 中的方法如下:
xxxxxxxxxx111methods: {2 // 添加操作3 add() {4 // 1. 如果要添加的任务名称为空,则提示警告信息5 if (this.taskName.length === 0) return console.warn('任务名称不能为空!')6 // 2. 向列表头部添加一个新任务7 this.todos.unshift({ task: this.taskName })8 // 3. 清空文本框的内容9 this.taskName = ''10 }11}模板结构如下:
xxxxxxxxxx211<div id="app">23 <!-- 添加区域 -->4 <div>5 <input type="text" v-model.trim="taskName">6 <button @click="add">添加</button>7 </div>89 <hr>10 <!-- 列表区域 -->11 <ul>12 <li v-for="item in todos">13 <!-- 任务的完成状态 -->14 <input type="checkbox">15 <!-- 任务的名称 -->16 <span>{{ item.task }}</span>17 <!-- 文本框备注 -->18 <input type="text">19 </li>20 </ul>21</div>问题还原出来之后,我们就要着手使用 key 值来解决临时状态紊乱的问题了,4个步骤如下:
步骤1:为 data 下的 todos 数据,添加唯一的 id 标识:
xxxxxxxxxx121data() {2 return {3 // 要添加的任务名称4 taskName: '',5 // 任务列表6 todos: [7 { id: 1, task: '晨练' },8 { id: 2, task: '吃早餐' },9 { id: 3, task: '吃午饭' }10 ]11 }12}步骤2:为了保证新增的任务项也能有递增的唯一的 id 值,我们需要在 data 下新增一个名为 nextId 的数据,它的值是下一次添加时可用的 id 值:
xxxxxxxxxx141data() {2 return {3 // 要添加的任务名称4 taskName: '',5 // 下一次添加时可用的 id 值6 nextId: 4,7 // 任务列表8 todos: [9 { id: 1, task: '晨练' },10 { id: 2, task: '吃早餐' },11 { id: 3, task: '吃午饭' }12 ]13 }14},步骤3:修改 methods下的 add 方法,为每次新增的任务附带唯一的 id 标识,添加完成后记得更新 nextId 的值:
xxxxxxxxxx131methods: {2 // 添加操作3 add() {4 // 1. 如果要添加的任务名称为空,则提示警告信息5 if (this.taskName.length === 0) return console.warn('任务名称不能为空!')6 // 2. 向列表头部添加一个新任务7 this.todos.unshift({ id: this.nextId, task: this.taskName })8 // 3. 清空文本框的内容9 this.taskName = ''10 // 4. 更新 nextId 的值11 this.nextId++12 }13}步骤4:修改模板结构中的 v-for 循环,在 v-for 循环的同时,为当前循环的元素动态绑定 key 属性的值,值为当前循环项的 id,即 item.id。代码如下:
xxxxxxxxxx111<!-- 列表区域 -->2<ul>3 <li v-for="item in todos" :key="item.id">4 <!-- 任务的完成状态 -->5 <input type="checkbox">6 <!-- 任务的名称 -->7 <span>{{ item.task }}</span>8 <!-- 文本框备注 -->9 <input type="text">10 </li>11</ul>到此为止,我们成功利用 v-for 配套的 key 值,解决了临时状态紊乱的问题。好处有2个:
简单粗暴的记忆方法:只要使用了 v-for 指令,就一定要加 key 值绑定;而且尽量用 id 当做 key 值,没有 id 的情况下找具有唯一性的 number、string 值当做 key。
虚拟 DOM 又叫做 VNode,本质上是用来描述 DOM 结构的 JavaScript 对象。它在 Vue 中用来描述不同类型的节点,例如普通元素节点、组件节点等。
例如,我们提供如下的 DOM 结构:
xxxxxxxxxx11<h1 id="topicTitle" title="标题">This is an article title.</h1>将其转化为 VNode 之后,格式如下:
xxxxxxxxxx91const vnode = {2 type: 'h1',3 props: {4 id: 'topicTitle',5 title: '标题'6 },7 // 子节点是普通文本8 children: 'This is an article title.'9}其中:
再例如,我们提供如下的 DOM 结构:
xxxxxxxxxx41<ul>2 <li>第1个子元素</li>3 <li>第2个子元素</li>4</ul>将其转化为 VNode 之后,格式如下:
xxxxxxxxxx151const vnode = {2 type: 'ul',3 props: {},4 // 子节点是 VNode 数组5 children: [{6 type: 'li',7 props: {},8 children: '第1个子元素'9 }, {10 type: 'li',11 props: {},12 children: '第2个子元素'13 }14 ]15}最大的优势:跨平台渲染,不同的平台可以基于自己的实现,轻松的把 VNode 转化为各自平台的代码
虚拟 DOM 配合 Diff 算法,能够尽可能减少 Vue 框架对 DOM 的操作

如上图所示,如果使用原生操作更新页面结构时,会移除所有旧 DOM 节点,并重新创建新 DOM 节点。
而我们可以明显的观察到,相比于旧 DOM,新 DOM 元素只是顺序上发生了变化:新 DOM 把 P3 移动到了 P1 之前。我们只需要更新 DOM 元素的先后顺序即可,根本不用重新创建所有的元素。
虚拟 DOM 配合 Diff 算法,可以实现旧 DOM 元素的复用,从而减少了不必要的 DOM 销毁和创建的过程。
误区:虚拟 DOM 的性能一定比手动操作原生 DOM 好。请注意,这个说法是错误的。

假设 data 中存在如下的一组 todos 数据,我们希望把它渲染为页面上的 ul 无序列表:
xxxxxxxxxx101data() {2 return {3 // 任务列表4 todos: [5 { task: '晨练' },6 { task: '吃早餐' },7 { task: '吃午饭' }8 ]9 }10}执行的 v-for 指令如下:
xxxxxxxxxx31<ul>2 <li v-for="item in todos">{{ item.task }}</li>3</ul>生成的虚拟 DOM (VNode)如下所示:
xxxxxxxxxx181const vnode = {2 type: 'ul',3 props: {},4 // VNode 数组,通过 v-for 指令渲染的子节点5 children: [{6 type: 'li',7 props: { key: null },8 children: '晨练'9 }, {10 type: 'li',11 props: { key: null },12 children: '吃早餐'13 }, {14 type: 'li',15 props: { key: null },16 children: '吃午饭'17 }]18}由于这是第一次渲染页面,所以会直接基于虚拟 DOM 创建真实的 DOM 结构,并把上一步生成的 VNode 标记为旧虚拟 DOM:
xxxxxxxxxx51<ul>2 <li>晨练</li>3 <li>吃早餐</li>4 <li>吃午饭</li>5</ul>至此,页面的首次渲染完成。
接下来,如果在 todos 数据的头部插入一个新任务,则对应的数据为:
xxxxxxxxxx111data() {2 return {3 // 任务列表4 todos: [5 { task: '测试' },6 { task: '晨练' },7 { task: '吃早餐' },8 { task: '吃午饭' }9 ]10 }11}生成的新虚拟 DOM (VNode)如下所示:
xxxxxxxxxx221const vnode = {2 type: 'ul',3 props: {},4 // VNode 数组,通过 v-for 指令渲染的子节点5 children: [{6 type: 'li',7 props: { key: null },8 children: '测试'9 }, {10 type: 'li',11 props: { key: null },12 children: '晨练'13 }, {14 type: 'li',15 props: { key: null },16 children: '吃早餐'17 }, {18 type: 'li',19 props: { key: null },20 children: '吃午饭'21 }]22}注意:为了提高 DOM 操作的性能,Vue 会尽可能的复用已存在的旧 DOM 元素,从而减少新 DOM 的创建。此时,Vue 会对新旧 VNode 进行对比。
那么对比的策略是什么呢?由于我们没有给 v-for 指令绑定 key 值,所以当前每个 li 对应的 VNode 的 key 默认等于 null。当 key 值为 null 的时候,Vue 的对比策略是:就地更新。
具体来讲,就地更新策略指的是逐层对比新旧 VNode 节点:

基于就地更新策略,上述的新旧 VNode 对比完成之后,Vue 得到的更新过程为:
掌握了 Vue 的就地更新策略之后,我们再用它分析一下 3.7.2 中的复选框勾选状态紊乱的 Bug。
v-for 的指令为:
xxxxxxxxxx81<ul>2 <li v-for="item in todos">3 <!-- 任务的完成状态 -->4 <input type="checkbox">5 <!-- 任务的名称 -->6 <span>{{ item.task }}</span>7 </li>8</ul>对应的数据为:
xxxxxxxxxx91data() {2 return {3 // 任务列表4 todos: [5 { task: '晨练' },6 { task: '吃早餐' }7 ]8 }9}首次渲染得到的 VNode 为:
xxxxxxxxxx281const vnode = {2 type: 'ul',3 props: {},4 // VNode 数组,通过 v-for 指令渲染的子节点5 children: [{ // 第1个li(VNode)6 type: 'li',7 props: { key: null },8 children: [{9 type: 'input',10 props: { type: 'checkbox' }11 }, {12 type: 'span',13 props: {},14 children: '晨练'15 }]16 }, { // 第2个li(VNode)17 type: 'li',18 props: { key: null },19 children: [{20 type: 'input',21 props: { type: 'checkbox' }22 }, {23 type: 'span',24 props: {},25 children: '吃早餐'26 }]27 }]28}至此,页面的首次渲染完成。
接下来,如果在 todos 数据的头部插入一个新任务,则对应的数据为:
xxxxxxxxxx101data() {2 return {3 // 任务列表4 todos: [5 { task: '测试' },6 { task: '晨练' },7 { task: '吃早餐' }8 ]9 }10}生成的新虚拟 DOM (VNode)如下所示:
xxxxxxxxxx391const vnode = {2 type: 'ul',3 props: {},4 // VNode 数组,通过 v-for 指令渲染的子节点5 children: [{ // 第1个li(VNode)6 type: 'li',7 props: { key: null },8 children: [{ 9 type: 'input',10 props: { type: 'checkbox' }11 }, {12 type: 'span',13 props: {},14 children: '测试'15 }]16 }, { // 第2个li(VNode)17 type: 'li',18 props: { key: null },19 children: [{20 type: 'input',21 props: { type: 'checkbox' }22 }, {23 type: 'span',24 props: {},25 children: '晨练'26 }]27 }, { // 第3个li(VNode)28 type: 'li',29 props: { key: null },30 children: [{31 type: 'input',32 props: { type: 'checkbox' }33 }, {34 type: 'span',35 props: {},36 children: '吃早餐'37 }]38 }]39}依据就地更新策略,新旧 VNode 具体的对比过程如下图所示:

经过分析发现,每个 checkbox 复选框的勾选状态,不影响就地更新的对比过程。复选框只是简单粗暴的被就地复用了。因此出现了临时状态紊乱的问题。
在 3.7.3 中,我们给复选框添加了 v-model 的双向数据绑定,解决了复选框勾选状态紊乱的问题。
这其实很好理解,因为 Vue 在进行新旧 VNode 对比的过程中,会根据数据驱动视图的思想,让真实 DOM 所显示的数据和 data 中的数据保持一致。
但是,临时状态不受数据驱动视图的影响,因为它没有与任何响应式数据进行绑定。
不同于默认的就地更新策略,key 值更新策略可以最大限度的实现已有元素的重排和重用。
在 key 值更新策略中,Vue 认为:
在 3.7.6 中,我们给 v-for 循环生成的 <li> 添加了 :key="item.id" 的 key 值绑定,从而解决了临时状态紊乱的问题。
在列表头部新增一个元素,则 key 值更新策略的执行过程如下:
初始 Diff 状态,定义三个变量:

第1次Diff时,默认从头部进行 VNode 的对比和复用。发现 j=0 指向的新旧 VNode 的 key 值不同,则放弃从头部 Diff,转而从尾部开始进行 Diff。从尾部进行Diff时,发现新旧 VNode 都是P3,于是复用P3,只对P3进行 patch 更新即可。同时,让 newEnd-- 和 oldEnd--,为第2次Diff做好准备:

第2次Diff时,newEnd=2 且 oldEnd=1,它们所指向的新旧 VNode 都是P2,key 值一样,可以复用,于是对P2进行 patch 更新。同时让 newEnd-- 和 oldEnd--,为第3次Diff做好准备:

第3次Diff时,newEnd=1 且 oldEnd=0,它们所指向的新旧 VNode 都是P1,key 值一样,可以复用,于是对P1进行 patch 更新。同时让 newEnd-- 和 oldEnd--,为第4次Diff做好准备:

第4次Diff时,newEnd=0 且 oldEnd=-1,同时 j=0。经过分析我们发现:
oldEnd < j,说明旧 VNode 已经处理完毕newEnd >= j,说明新 VNode 尚未处理完,证明这些尚未处理完的新 Node 是新增的,只需创建并添加到P1之前即可
此次 Diff 过程中,完全复用了 P1 – P3 这三个节点,只新建了 P4 节点。
初始状态下,定义三个变量,分别指向头部和两个尾部节点:

先从头部进行 Diff 比较,如果 key 值相同,则进行 patch 更新:

第3次Diff时 j=2,此时新 VNode 是P4,旧 VNode 是P3,key 值不同,则从尾部开始 Diff:

第4次Diff时,newEnd=2 且 oldEnd=1,同时 j=2。经过分析我们发现:
oldEnd < j,说明旧 VNode 已经处理完毕newEnd >= j,说明新 VNode 尚未处理完,证明这些尚未处理完的新 Node 是新增的,只需创建并添加到P3之前即可
此次 Diff 过程中,完全复用了 P1-P3 节点,只新建了一个 P4 节点。
初始状态下,定义三个变量,分别指向头部和两个尾部节点:

先从头部进行比较,如果 key 值相同,则进行 patch 更新:

第3次Diff时 j=2,此时新 VNode 是P4,旧 VNode 是P3,key 值不同,则从尾部开始 Diff:

第4次Diff时,newEnd=1 且 oldEnd=2,同时 j=2。经过分析我们发现:
newEnd < j,说明新 VNode 已经处理完毕oldEnd >= j,说明旧 VNode 尚未处理完,证明这些尚未处理完的旧 Node 是多余的,只需删除它们即可
此次 Diff 过程中,完全复用了 P1,P2,P4 这三个节点,只删除了一个 P3 节点。
在理想状态下,Diff 算法只需要简单的挂载或卸载节点即可,但在复杂情况下,简单的挂载和卸载节点可能无法处理到所有的节点,例如:
初始状态如下:

进行先头后尾的 Diff 过程:

当头和尾都处理完成后,我们发现还剩下了很多中间的节点没有被处理到:

为了进一步处理中间这些剩余的节点,我们需要了解一下 Diff 算法中最核心、最重要的内容:
为新 VNode 中未处理的元素建立索引表:

循环旧 VNode,判断是否需要进行移动:

创建 source 数组,用来记录新 VNode 在旧 VNode 中对应的真实的索引位置:

循环旧 VNode,填充 source 数组:

根据 source 数组得到最长递增子序列的索引数组:

倒序循环每一个新 VNode 节点,根据不同的条件对 DOM 进行创建或移动:
